package com.example.servlet;

import com.example.pojo.Handler;
import com.example.utils.ServletFactory;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;

/**
 * 初始化
 * 1.加载配置文件 springmvc.properties 自动处理 classpath 字符串
 * 2.获取封装好的 url和Method之间的映射关系 的集合
 * 3.根据url获取到能够处理当前请求的hanlder
 * 4.参数绑定
 * 5.遍历request中所有参数
 * 6.处理权限控制，无权限返回 401 not authority
 * 7.正常返回传递的参数，到浏览器
 * 8.新增检验权限方法 verifySecurity（）
 * security 注解必须传递参数
 * 分两种情况：
 * 1.class security 为null 或传值长度=0
 * （1）方法上有security注解，只放行符合的请求
 * （2）方法上没有security注解 或 或传值长度=0，全部放行
 * 2.class security 有值 对当前类每个方法，放行符合设定值的请求
 * （1）方法上有security注解，且该值 不再 类注解上，增加当前方法放行条件
 * （2）方法上没有security注解 或传值长度=0 ，只放行，符合类security注解 值的请求
 *
 * @author kangshuai
 */
public class DispatcherServlet extends HttpServlet {

    private List<Handler> handlerMapping = new ArrayList<>();

    /**
     * 初始化
     * 1.加载配置文件 springmvc.properties 自动处理 classpath 字符串
     * 2.获取封装好的 url和Method之间的映射关系 的集合
     *
     * @param config
     */
    @Override
    public void init(ServletConfig config) throws ServletException {

        String contextConfigLocation = config.getInitParameter("contextConfigLocation");
        try {
            handlerMapping = ServletFactory.build(contextConfigLocation.contains("classpath") ?
                    contextConfigLocation.split(":")[1] : contextConfigLocation);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doPost(req, resp);
    }

    /**
     * 1.根据url获取到能够处理当前请求的hanlder
     * 2.参数绑定
     * 3.遍历request中所有参数
     * 4.处理权限控制，无权限返回 401 not authority
     * 5.正常返回传递的参数，到浏览器
     *
     * @param req
     * @param resp
     * @throws ServletException
     * @throws IOException
     */
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 处理请求：根据url，找到对应的Method方法，进行调用
        // 根据url获取到能够处理当前请求的hanlder
        Handler handler = getHandler(req);

        if (handler == null) {
            resp.getWriter().write("404 not found");
            return;
        }

        // 参数绑定
        // 获取所有参数类型数组，这个数组的长度就是我们最后要传入的args数组的长度
        Class<?>[] parameterTypes = handler.getMethod().getParameterTypes();


        // 根据上述数组长度创建一个新的数组（参数数组，是要传入反射调用的）
        Object[] paraValues = new Object[parameterTypes.length];

        // 以下就是为了向参数数组中塞值，而且还得保证参数的顺序和方法中形参顺序一致

        Map<String, String[]> parameterMap = req.getParameterMap();

        // 遍历request中所有参数  （填充除了request，response之外的参数）
        for (Map.Entry<String, String[]> param : parameterMap.entrySet()) {
            if (param.getKey().equals("name")) {
                if (!verifySecurity(handler, ArrayUtils.toString(param.getValue()))) {
                    resp.getWriter().write("401 not authority");
                    return;
                }
            }
            // name=1&name=2   name [1,2]
            String value = StringUtils.join(param.getValue(), ",");

            // 如果参数和方法中的参数匹配上了，填充数据
            if (!handler.getParamIndexMapping().containsKey(param.getKey())) {
                continue;
            }

            // 方法形参确实有该参数，找到它的索引位置，对应的把参数值放入paraValues
            Integer index = handler.getParamIndexMapping().get(param.getKey());

            // 把前台传递过来的参数值填充到对应的位置去
            paraValues[index] = value;

        }


        int requestIndex = handler.getParamIndexMapping().get(HttpServletRequest.class.getSimpleName());
        paraValues[requestIndex] = req;


        int responseIndex = handler.getParamIndexMapping().get(HttpServletResponse.class.getSimpleName());
        paraValues[responseIndex] = resp;

        // 最终调用handler的method属性
        try {
            resp.getWriter().write((String) handler.getMethod().invoke(handler.getController(), paraValues));
        } catch (Exception e) {
            e.printStackTrace();
            resp.getWriter().write("500 code exception");
        }

    }

    private Handler getHandler(HttpServletRequest req) {
        if (handlerMapping.isEmpty()) {
            return null;
        }
        String url = req.getRequestURI();
        for (Handler handler : handlerMapping) {
            Matcher matcher = handler.getPattern().matcher(url);
            if (!matcher.matches()) {
                continue;
            }
            return handler;
        }
        return null;
    }

    /**
     * security 注解必须传递参数
     * 分两种情况：
     * 1.class security 为null 或 传值长度=0
     * （1）方法上有security注解，只放行符合的请求
     * （2）方法上没有security注解，全部放行
     * 2.class security 有值 对当前类每个方法，放行符合设定值的请求
     * （1）方法上有security注解，且该值 不再 类注解上，增加当前方法放行条件
     * （2）方法上没有security注解，只放行，符合类security注解 值的请求
     *
     * @param handler
     * @param name
     * @return
     */
    private Boolean verifySecurity(Handler handler, String name) {
        name = name.substring(1, name.length() - 1);
        if (null != handler.getTypeSecurity() && handler.getTypeSecurity().length > 0) {
            if (null != handler.getMethodSecurity() && handler.getMethodSecurity().length > 0) {
                if (ArrayUtils.contains(handler.getMethodSecurity(), name)) {
                    return true;
                }
            }
            if (ArrayUtils.contains(handler.getTypeSecurity(), name)) {
                return true;
            }
        } else {
            if (null != handler.getMethodSecurity() && handler.getMethodSecurity().length > 0) {
                if (ArrayUtils.contains(handler.getMethodSecurity(), name)) {
                    return true;
                } else {
                    return false;
                }
            }
            return true;
        }
        return false;
    }

}
